package com.lg.distributed.idempotence.aspect;

import com.lg.distributed.idempotence.annotation.AutoIdempotent;
import com.lg.distributed.idempotence.exception.GlobalException;
import com.lg.distributed.idempotence.utils.IdWorker;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Collections;

/**
 * AOP 实现接口幂等注解的一键幂等
 *
 * @author by Mr. Li 2020/12/10 17:27
 */
@Aspect
@Component
public class ApiIdempotentAspect {

    /**
     * 实现 redis 的 get delete 原子性 的lua 脚本
     */
    private static final String RELEASE_LOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IdWorker idWorker;


    /**
     * 切面
     */
    @Pointcut("@annotation(com.lg.distributed.idempotence.annotation.AutoIdempotent)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取 使用注解的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        AutoIdempotent autoIdempotent = method.getDeclaredAnnotation(AutoIdempotent.class);
        if (autoIdempotent != null) {
            return apiIdempotent(joinPoint, signature, method, autoIdempotent);
        }
        // 放行
        Object proceed = joinPoint.proceed();
        return proceed;
    }

    /**
     * 校验token
     *
     * @param joinPoint
     * @param signature
     * @param method
     * @param autoIdempotent
     * @return
     * @throws Throwable
     */

    private Object apiIdempotent(ProceedingJoinPoint joinPoint, MethodSignature signature, Method method, AutoIdempotent autoIdempotent) throws Throwable {
        if (autoIdempotent == null) {
            // 直接放行
            return joinPoint.proceed();
        }
        // 1. 生成 全局唯一id
        String uniqueId = String.valueOf(idWorker.nextId());
        // 当前接口就对应着当前id
        String prefix = autoIdempotent.prefix();
        if (StringUtils.isBlank(prefix)) {
            throw new GlobalException("CacheLock prefix can't be null");
        }
        // 拼接 key
        String delimiter = autoIdempotent.delimiter();

        StringBuilder sb = new StringBuilder();
        final String lockKey = sb.append(prefix).append(delimiter).append(method.getName()).toString();

        try {
            // 加锁
            Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, uniqueId, autoIdempotent.expire(), autoIdempotent.timeUnit());
            if (!result) {
                // 设置失败
                throw new GlobalException("请勿重复提交");
            }
            return joinPoint.proceed();
        } finally {
            // 释放锁。
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT, Long.class);
            Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList(lockKey), uniqueId);
        }
    }

    // 这里提供另一种方案。
    /*
     * 假设，在nginx 的请求中设置了，id标识。那么在此可以进行请求拦截。实现请求携带token。
     * 并且如果该token是有序的。还可以配合队列 实现有序的网络请求
     * */
    public HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request;
    }
}
